/***************************************************
 * mode name:L1WeightMatrix
 * brief:   coding: UTF-8
 * @author yxt
 * @created 2024/01/23 15:18:59
 * @imfor ~/bigSystem/Emotion/src/L1WeightMatrix.cpp
 * @modify:利用L1Norm作为度量，构建两个样本之间距离邻接矩阵W
 * 		
***************************************************/
#include <iostream>

#include "L1WeightMatrix.h"
#include "ColorfulLog.h"

// #define DEBUG

/**********************************
 * 函数名：L1WeightMatrix::L1WeightMatrix
 * 功能：获取邻接矩阵
 * 输入：无
 * 输出：无
 * 备注：仅支持KNN,更多详情请查看源码:L1WeightMatrix.m
**********************************/
L1WeightMatrix::L1WeightMatrix(arma::mat &fea,
                                std::string metric, double PCARatio, 
                                L1WM::NeighborMode_enum neighborMode, int k,  
                                L1WM::WeightMode_enum weightMode, int t,
                                const arma::vec &gnd
                                ):
                metric(metric),
                PCARatio(PCARatio),
                neighborMode(neighborMode),
                k(k),
                weightMode(weightMode),
                t(t),
                gnd(gnd)
{
    // 使用的参数
    // metric = std::string("Euclidean");
    // PCARatio = 0.9900;
    // neighborMode = L1WM::KNN;
    // k = 20;
    // weightMode = L1WM::HeatKernel;
    // t = 3;
    bSpeed  = 1;
    bNormalized = 0;
    bLDA = 0;
    bSelfConnected = bLDA ? 1 : 0;


    if(!metric.empty()){
        LOG_WARN("This function has been changed and the Metric is no longer be supported\n");
    }
    
    switch (neighborMode)
    {
        case L1WM::KNN:
            if(k != 0){
                this->k = k;
            }
            else{
                k = 5;
            }
            break;

        case L1WM::Supervised:
            // Supervised模式，判断参数有效性
            // gnd
            if(gnd.is_empty()){
                LOG_ERR("Label(gnd) should be provided under \"Supervised\" \"NeighborMode\"!!\n");
                throw "In L1WeightMatrix::L1WeightMatrix, lack label(gnd)!!";
            }
            if(fea.n_rows != gnd.n_elem){
                LOG_ERR("gnd doesn\'t match with fea!\n");
                printf("\" fea\":%lldx%lld; \"gnd\":%lldx%lld\n",fea.n_rows,fea.n_cols,gnd.n_rows,gnd.n_cols);
                throw "In L1WeightMatrix::L1WeightMatrix, gnd doesn\'t match with fea!";
            }
            break;
        
        default:
            LOG_ERR("NeighborMode does not exist!\n");
            throw "In L1WeightMatrix::L1WeightMatrix, NeighborMode parameter error!";
            break;
    }

    bBinary = 0;
    bCosine = 0;

    switch (weightMode){
        case L1WM::Binary:
            bBinary = 1;
            break;

        case L1WM::HeatKernel:
            // matlab源码，未移植，t默认为1
            // if ~isfield(options,'t')
            //     nSmp = size(fea,1);
            //     if nSmp > 3000
            //         D = L1Dist(fea(randsample(nSmp,3000),:));
            //     else
            //         D = L1Dist(fea);
            //     end
            //     options.t = mean(mean(D));
            // end
            break;

        case L1WM::Cosine:
            bCosine = 1;
            break;
        
        default:
            LOG_ERR("WeightMode does not exist!!\n");
            throw "In L1WeightMatrix::L1WeightMatrix, WeightMode parameter error!";
            break;
    }

    arma::uword nSmp = 0;
    // 若没有输入gnd
    if(gnd.is_empty()){
        nSmp = fea.n_rows;
    }
    else{
        nSmp = gnd.n_elem;
    }

    long maxM = 62500000; // 500M
    // 向下取整
    long BlockSize = maxM/(nSmp*3);

    if(neighborMode == L1WM::Supervised){
        arma::vec label = arma::unique(gnd);
        int label_len = label.n_elem;
        if(bLDA){
            arma::mat G(nSmp,nSmp,arma::fill::zeros);
            for (int index=0; index<label_len; ++index){
                // 未写完，后续再写
            }
        }

    }

    if(neighborMode == L1WM::KNN && k>0){
        // printf("KNN_MOD\n");
        if(!(bCosine && bNormalized)){
            // G:[7x(20+1),3]
            arma::mat G(nSmp*(k+1),3, arma::fill::zeros);
            // nSmp/BlockSize
            int block_num = std::ceil((double)nSmp/BlockSize);
            #ifdef DEBUG
            printf("block_num=%d\n",block_num);
            #endif // DEBUG
            for (int block_index=0; block_index<block_num; ++block_index){
                if(block_index == block_num-1){
                    // 生成数据索引向量
                    // 最后一个块，直接包含所有剩余向量,生成子矩阵的行索引
                    // linspace:给定起始,结束和元素个数,每个元素均匀分布
                    // regspace:给定起始,结束和步长
                    arma::uvec smpIdx = arma::regspace<arma::uvec>(block_index*BlockSize,nSmp-1);
                    // 生成子矩阵的列索引,这里选择所有列
                    arma::uvec colIdx = arma::regspace<arma::uvec>(0,fea.n_cols-1);
                    // smpIdx.print("smpIdx:");
                    // colIdx.print("colIdx:");
                    // printf("smpIdx.n_elem=%lld\n",smpIdx.n_elem);
                    // printf("nSmp=%lld\n",nSmp);

                    arma::mat dist(fea.n_rows,fea.n_rows,arma::fill::zeros);
                    // arma::mat dist(7,7,arma::fill::zeros);
                    // 矩阵分块
                    arma::mat smpData = fea.submat(smpIdx,colIdx);
                    // smpData.print("smpData");
                    // 计算向量两两之间的一范数
                    L1Dist(smpData,dist);
                    #ifdef DEBUG
                    dist.print("dist:");
                    #endif

                    int nSmpNow = smpIdx.n_elem;
                    // arma::mat dump = arma::zeros<arma::mat>(nSmpNow,k+1);
                    arma::mat dump(nSmpNow,k+1,arma::fill::zeros);
                    arma::umat idx(nSmpNow,k+1,arma::fill::zeros);

                    if(bSpeed){
                        arma::uvec nSmpNow_regspace = arma::regspace<arma::uvec>(0,nSmpNow-1);
                        for (int k_index=0; k_index<k+1; ++k_index){
                            // 获取每一行的最小值和最小值所在列的索引
                            dump.col(k_index) = arma::min(dist,1);
                            idx.col(k_index) = arma::index_min(dist,1);
                            arma::uvec temp = idx.col(k_index)*nSmpNow + nSmpNow_regspace;
                            // 取出后,原来的位置用10^10填充,为下一次取出次小值做准备
                            dist.elem(temp).fill(std::pow(10,10));
                        }
                    }
                    else{
                        // 没用到,后续再补充完整
                        // matlab源码
                        //  [dump idx] = sort(dist,2); % sort each row
                        // idx = idx(:,1:options.k+1);
                        // dump = dump(:,1:options.k+1);
                    }

                    if (!bBinary){
                        if(bCosine){
                            // 没用到,后续再补充完整
                            // matlab源码
                            // dist = Normfea(smpIdx,:)*Normfea';
                            // dist = full(dist);
                            // linidx = [1:size(idx,1)]';
                            // dump = dist(sub2ind(size(dist),linidx(:,ones(1,size(idx,2))),idx));
                        }
                        else{
                            dump = arma::exp(-dump/(2*std::pow(t,2)));
                        }
                    }

                    // 将smpIdx扩展成k+1行1列,赋值给G
                    // 最后一个块,G填充时,起始序号为块起始索引,结束序号为样本个数
                    // arma::uvec row_index = arma::regspace<arma::uvec>(block_index*BlockSize*(k+1),nSmp*(k+1)-1);
                    arma::mat smpIdx_extend = repmat(arma::conv_to<arma::vec>::from(smpIdx), k+1, 1);
                    // G的第一列循环存放样本数序号
                    G.submat(block_index*BlockSize*(k+1),0,nSmp*(k+1)-1,0) = smpIdx_extend;
                    // G的第二列循环存放最(次)小值索引
                    G.submat(block_index*BlockSize*(k+1),1,nSmp*(k+1)-1,1) = arma::conv_to<arma::mat>::from(arma::vectorise(idx));
                    // 第三列循环存放经过指数运算的最(次)小值
                    if(!bBinary){
                        G.submat(block_index*BlockSize*(k+1), 2, nSmp*(k+1)-1, 2) = arma::vectorise(dump);
                    }
                    else{
                        G.submat(block_index*BlockSize*(k+1), 2, nSmp*(k+1)-1, 2).fill(1);
                    }
                    // G.print("G:");

                }
                else{
                    // 多个块,还没写后续写
                }
            }
            // 根据G创建稀疏矩阵
            arma::umat locations(2,G.n_rows);
            // W行索引
            locations.row(0) = arma::conv_to<arma::uvec>::from(G.col(0)).t();
            // W列索引
            locations.row(1) = arma::conv_to<arma::uvec>::from(G.col(1)).t();
            // 根据G创建稀疏矩阵
            W = arma::sp_mat(locations,G.col(2),nSmp,nSmp);
        }

        if(bBinary){
            // 将W中逻辑为真的元素赋值为1

            // matlab源码
            // W(logical(W)) = 1;
        }
        // 跳过471-486行,未用到
        // if isfield(options,'bSemiSupervised') && options.bSemiSupervised
        
        // 去除对角线
        if (!bSelfConnected){
            if(W.is_empty()){
                throw "In L1WeightMatrix::L1WeightMatrix, W is empty!";
            }
            W -= arma::diagmat(W);
        }

        #if 1
        W = arma::max(W,W.t());
        #endif
    }
}

L1WeightMatrix::~L1WeightMatrix()
{
}

arma::sp_mat L1WeightMatrix::get_weightMatrix_sp(){
    return W;
}

arma::mat L1WeightMatrix::get_weightMatrix_full(){
    return arma::conv_to<arma::mat>::from(W);
}

void L1WeightMatrix::L1Dist(arma::mat Data,arma::mat& Distance){
    // 判断接收的矩阵是否是空矩阵,空则初始化
    if(Distance.is_empty()){
    // if(Distance.n_rows == 0 || Distance.n_cols == 0){
        Distance = arma::zeros<arma::mat>(Data.n_rows, Data.n_rows);
    }

    // 向量两两作差,求向量一范数,获取上三角
    for(int Order=0; Order<Data.n_rows-1; ++Order){
        for(int Order1=Order+1; Order1<Data.n_rows; ++Order1){
            arma::rowvec temp_vec = Data.row(Order)-Data.row(Order1);
            double L1 = arma::norm(temp_vec,1);
            Distance(Order,Order1) = L1;
            // Distance(Order1,Order) = L1;
        }
    }
    // Distance.print("Distance:");
    // 补充完整,输出实对称阵
    Distance += Distance.t();
}

void L1WeightMatrix::showPara(){
    // printf("======= L1WeightMatrix Parameter =========\n");
    std::cout << "======= L1WeightMatrix Parameter =========" << std::endl;
    std::cout << "  metric:" << metric << std::endl;
    std::cout << "  PCARatio:" << PCARatio << std::endl;
    std::cout << "  neighborMode:" << neighborMode << "( 1:KNN; 2:Supervised)" << std::endl;
    std::cout << "  k:" << k << std::endl;
    std::cout << "  weightMode:" << weightMode << "( 1:Binary; 2:HeatKernel; 3:Cosine)" << std::endl;
    std::cout << "  t:" << t << std::endl;
    
    if(gnd.is_empty()){
        std::cout << "  gnd vector is empty." << std::endl;
    }
    else{
        gnd.t().print(" gnd.t():");
    }
    std::cout << "======= L1WeightMatrix Parameter =========" << std::endl;


}


